using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using Kopernikus.Parsing.GenericLexing;
using Kopernikus.Parsing.Utility;

namespace Kopernikus.Parsing.Lexing
{
    public class Lexer<TToken, TAction> : ILexer<TToken>
    {
        private readonly Dictionary<string, TAction> m_defaultActions;
        private readonly Dictionary<string, TAction> m_keywordActions;
        private readonly List<Pair<string, TAction>> m_matchActions = new List<Pair<string, TAction>>();
        private readonly Regex m_regex;

        public Lexer(Dictionary<string, TAction> actions, Dictionary<string, TAction> defaultActions,
                     Dictionary<string, TAction> keywordActions)
        {
            m_defaultActions = new Dictionary<string, TAction>(defaultActions);
            m_keywordActions = new Dictionary<string, TAction>(keywordActions);

            StringBuilder sb = new StringBuilder();
            foreach (KeyValuePair<string, TAction> p in actions)
            {
                string actionName = string.Format("a{0}", m_matchActions.Count);
                string groupedRegex = string.Format("(?'{0}'\\A{1})", actionName, p.Key);

                if (m_matchActions.Count > 0)
                {
                    sb.Append('|');
                }
                sb.Append(groupedRegex);

                m_matchActions.Add(Pair<string, TAction>.Create(actionName, p.Value));
            }

            m_regex = new Regex(sb.ToString(), RegexOptions.Multiline);
        }

        #region ILexer<TToken> Members

        public IEnumerable<TToken> Scan(string input)
        {
            return Scan(input);
        }

        #endregion

        private TAction GetEffectiveAction(TAction action, string value)
        {
            TAction keywordAction;
            return m_keywordActions.TryGetValue(value, out keywordAction) ? keywordAction : action;
        }

        public IEnumerable<TToken> Scan(ILexerEvaluator<TToken, TAction> evaluator, string input)
        {
            int inputPos = 0;

            int matchLine = 0;
            int matchCol = 0;

            while (inputPos < input.Length)
            {
                TAction action = default(TAction);
                string value = null;

                if (
                    !(MatchRegexp(input, inputPos, ref action, ref value) ||
                      MatchDefault(input, inputPos, ref action, ref value)))
                {
                    throw new InvalidInputException(input, inputPos, matchLine, matchCol);
                }

                TAction effectiveAction = GetEffectiveAction(action, value);
                TToken token;
                if (evaluator.TryEvaluate(
                    effectiveAction,
                    value,
                    inputPos, matchLine, matchCol, out token))
                {
                    yield return token;
                }

                int matchLength = value.Length;

                if (inputPos + matchLength < input.Length)
                {
                    int lastLineBreakPos = input.LastIndexOf('\n', inputPos + matchLength, matchLength);
                    if (-1 == lastLineBreakPos)
                    {
                        matchCol += matchLength;
                    }
                    else
                    {
                        matchCol = inputPos + matchLength - lastLineBreakPos - 1;

                        while (true)
                        {
                            ++matchLine;

                            --lastLineBreakPos;
                            if (lastLineBreakPos <= inputPos)
                            {
                                break;
                            }
                            lastLineBreakPos =
                                input.LastIndexOf('\n', lastLineBreakPos, lastLineBreakPos - inputPos);
                        }
                    }
                }

                inputPos += matchLength;
            }
        }

        private bool MatchDefault(string input, int pos, ref TAction action, ref string value)
        {
            int maxTestLength = input.Length - pos;
            if (maxTestLength == 0)
            {
                return false;
            }

            foreach (KeyValuePair<string, TAction> pair in m_defaultActions)
            {
                // TODO: Could use KMP algorithm here...

                string testString = pair.Key;

                if (string.IsNullOrEmpty(testString))
                {
                    continue;
                }
                if (testString.Length > maxTestLength)
                {
                    continue;
                }

                bool isMatch = true;
                for (int i = 0; i < testString.Length; ++i)
                {
                    if (testString[i] != input[pos + i])
                    {
                        isMatch = false;
                        break;
                    }
                }
                if (isMatch)
                {
                    action = pair.Value;
                    value = testString;
                    return true;
                }
            }

            if (m_defaultActions.TryGetValue(string.Empty, out action))
            {
                value = input.Substring(pos, 1);
                return true;
            }
            return false;
        }

        private bool MatchRegexp(string input, int pos, ref TAction action, ref string value)
        {
            if (m_matchActions.Count > 0)
            {
                Match match = m_regex.Match(input, pos, input.Length - pos);
                if (match.Success)
                {
                    foreach (Pair<string, TAction> pair in m_matchActions)
                    {
                        Group g = match.Groups[pair.First];

                        if (g.Success)
                        {
                            value = g.Value;
                            action = pair.Second;
                            return true;
                        }
                    }
                }
            }
            return false;
        }
    }
}